﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows;

// Based on code from MVVM Foundation on CodePlex: http://mvvmfoundation.codeplex.com/
// License is MS-Pl

namespace MishraReader
{
    public static class PropertyObserver
    {
        public static PropertyObserver<T> Create<T>(T target) where T : INotifyPropertyChanged
        {
            Contract.Requires<ArgumentNullException>(target != null, "target");

            return new PropertyObserver<T>(target);
        }
    }

    /// <summary>
    ///   Monitors the PropertyChanged event of an object that implements INotifyPropertyChanged, and executes callback methods (i.e. handlers) registered for properties of that object.
    /// </summary>
    /// <typeparam name="TPropertySource"> The type of object to monitor for property changes. </typeparam>
    public sealed class PropertyObserver<TPropertySource> where TPropertySource : INotifyPropertyChanged
    {
        private readonly Dictionary<string, Action<TPropertySource>> _propertyNameToHandlerMap;
        private readonly WeakReference _propertySourceRef;
        private readonly PropertyChangedEventHandler _handler;

       /// <summary>
        ///   Initializes a new instance of PropertyObserver, which observes the 'propertySource' object for property changes.
        /// </summary>
        /// <param name="propertySource"> The object to monitor for property changes. </param>
        public PropertyObserver(TPropertySource propertySource)
        {
            Contract.Requires<ArgumentNullException>(propertySource != null, "propertySource");

            _propertySourceRef = new WeakReference(propertySource);
            _propertyNameToHandlerMap = new Dictionary<string, Action<TPropertySource>>();

            _handler = new PropertyChangedEventHandler(OnReceiveEvent).MakeWeak(c => propertySource.PropertyChanged -= c);
           propertySource.PropertyChanged += _handler;
        }

        /// <summary>
        ///   Registers a callback to be invoked when the PropertyChanged event has been raised for the specified property.
        /// </summary>
        /// <param name="expression"> A lambda expression like 'n => n.PropertyName'. </param>
        /// <param name="handler"> The callback to invoke when the property has changed. </param>
        /// <returns> The object on which this method was invoked, to allow for multiple invocations chained together. </returns>
        public PropertyObserver<TPropertySource> RegisterHandler(
            Expression<Func<TPropertySource, object>> expression,
            Action<TPropertySource> handler)
        {
            var propertyName = GetPropertyName(expression);

            return RegisterHandler(propertyName, handler);
        }

        public PropertyObserver<TPropertySource> RegisterHandler(
            string propertyName,
            Action<TPropertySource> handler)
        {
            Contract.Requires<ArgumentNullException>(handler != null, "handler");
            
            propertyName = propertyName ?? string.Empty;

            var propertySource = GetPropertySource();
            if (propertySource != null)
            {
                Contract.Assert(!_propertyNameToHandlerMap.ContainsKey(propertyName), "Why is the '" + propertyName + "' property being registered again?");

                _propertyNameToHandlerMap[propertyName] = handler;

            }

            return this;
        }

        public void UnregisterAllHandlers()
        {
            var propertySource = GetPropertySource();
            if (propertySource != null)
            {
                var properties = _propertyNameToHandlerMap.Keys.ToArray();
                
                properties.ForEach(s => _propertyNameToHandlerMap.Remove(s));

                propertySource.PropertyChanged -= _handler;
            }
        }

        /// <summary>
        ///   Removes the callback associated with the specified property.
        /// </summary>
        /// <param name="expression"> A lambda expression like 'n => n.PropertyName'. </param>
        /// <returns> The object on which this method was invoked, to allow for multiple invocations chained together. </returns>
        public PropertyObserver<TPropertySource> UnregisterHandler(Expression<Func<TPropertySource, object>> expression)
        {
            var propertyName = GetPropertyName(expression);

            return UnregisterHandler(propertyName);
        }

        public PropertyObserver<TPropertySource> UnregisterHandler(string propertyName)
        {
            propertyName = propertyName ?? string.Empty;
            var propertySource = GetPropertySource();
            if (propertySource != null)
            {
                if (_propertyNameToHandlerMap.ContainsKey(propertyName))
                {
                    _propertyNameToHandlerMap.Remove(propertyName);
                }
            }

            return this;
        }

        private TPropertySource GetPropertySource()
        {
            try
            {
                return (TPropertySource)_propertySourceRef.Target;
            }
            catch
            {
                return default(TPropertySource);
            }
        }

        private void OnReceiveEvent(object sender, PropertyChangedEventArgs args)
        {
            if (args != null && sender is TPropertySource)
            {
                var propertyName = args.PropertyName;
                var propertySource = (TPropertySource)sender;

                if (String.IsNullOrEmpty(propertyName))
                {
                    // When the property name is empty, all properties are considered to be invalidated.
                    // Iterate over a copy of the list of handlers, in case a handler is registered by a callback.
                    foreach (var handler in _propertyNameToHandlerMap.Values.ToArray())
                        handler(propertySource);
                }
                else
                {
                    Action<TPropertySource> handler;
                    if (_propertyNameToHandlerMap.TryGetValue(propertyName, out handler))
                    {
                        handler(propertySource);
                    }
                }
            }
        }


        internal static string GetPropertyName(Expression<Func<TPropertySource, object>> expression)
        {
            if (expression == null)
                return string.Empty;

            var lambda = expression as LambdaExpression;
            MemberExpression memberExpression;
            if (lambda.Body is UnaryExpression)
            {
                var unaryExpression = lambda.Body as UnaryExpression;
                memberExpression = unaryExpression.Operand as MemberExpression;
            }
            else
            {
                memberExpression = lambda.Body as MemberExpression;
            }

            Contract.Assert(memberExpression != null, "Please provide a lambda expression like 'n => n.PropertyName'");

            if (memberExpression != null)
            {
                var propertyInfo = memberExpression.Member as PropertyInfo;

                return propertyInfo.Name;
            }

            return null;
        }
    }
}